import heapq

import math
import numpy as np
from scipy.spatial import ConvexHull

from data_preparation.trajectory import Trajectory
from data_preparation.trajectory_set import TrajectorySet
from discretization.grid import Grid


class Test:
    def __init__(self):
        pass

    # ################
    # density_experiment
    ##################

    def density_experiment(self, trajectory_set1: TrajectorySet, synthetic_trajectory: list, queries: list):
        trajectory_number = trajectory_set1.trajectory_number
        index = trajectory_number * 0.01

        origin_density = list()
        synthetic_desity = list()
        for q in queries:
            origin_density.append(q.get_origin_density(trajectory_set1))#去重
            synthetic_desity.append(q.get_synthetic_density(synthetic_trajectory))

        origin_density = np.asarray(origin_density)
        synthetic_desity = np.asarray(synthetic_desity)

        numerator = np.abs(origin_density - synthetic_desity)

        denominator = np.asarray([max(origin_density[i], index) for i in range(len(origin_density))])

        error_arr = numerator / denominator
        error = np.mean(error_arr)

        print("density_error:", error)

    def calculate_origin_density(self, trajectory_set1: TrajectorySet, queries: list) -> list:
        density_list = []
        for q in queries:
            density_list.append(q.get_origin_density(trajectory_set1))
        return density_list

    def calculate_synthetic_density(self, synthetic_trajectory: list, queries: list) -> list:
        density_list = []
        for q in queries:
            density_list.append(q.get_synthetic_density(synthetic_trajectory))
        return density_list

    ####################
    # length_experiment
    ####################

    def length_experiment(self, trajectory_set1: TrajectorySet, real_tra_list: list, grid: Grid, bucket_num=50):

        origin_length = [self.get_real_length(t) for t in trajectory_set1.trajectory_list]#real_point_trajectory未去重
        synthetic_length = [self.get_synthetic_length(t) for t in real_tra_list]

        bucket_size = (max(origin_length) - min(origin_length)) / bucket_num

        orig_count = np.zeros(bucket_num)
        syn_count = np.zeros(bucket_num)
        for i in range(bucket_num):
            start = i * bucket_size
            end = start + bucket_size

            for d in origin_length:
                if start <= d <= end:
                    orig_count[i] += 1
            for d in synthetic_length:
                if start <= d <= end:
                    syn_count[i] += 1

        orig_count /= np.sum(orig_count)
        syn_count /= np.sum(syn_count)

        error = self.jensen_shannon_distance(orig_count, syn_count)
        print("length_error:", error)

    def get_synthetic_length(self, t):
        dist = 0
        for i in range(len(t) - 1):
            curr_p = t[i]
            next_p = t[i + 1]
            dist += self.euclidean_distance(curr_p, next_p)
        return dist

    def get_real_length(self, T: Trajectory):
        t = T.real_point_trajectory
        dist = 0
        for i in range(len(t) - 1):
            curr_p = t[i]
            next_p = t[i + 1]
            dist += self.euclidean_distance(curr_p, next_p)
        return dist

    def jensen_shannon_distance(self, prob1, prob2):
        prob1 = np.asarray(prob1)
        prob2 = np.asarray(prob2)

        avg_prob = (prob1 + prob2) / 2

        return 0.5 * self.kl_divergence(prob1, avg_prob) + 0.5 * self.kl_divergence(prob2, avg_prob)

    def kl_divergence(self, prob1, prob2):
        prob1 = np.asarray(prob1)
        prob2 = np.asarray(prob2)

        kl = np.log((prob1 + 1e-8) / (prob2 + 1e-8)) * prob1

        return np.sum(kl)
        # kl = 0
        # prob1 = np.asarray(prob1)
        # prob2 = np.asarray(prob2)
        # for i in range(len(prob1)):
        #     if prob1[i] != 0.0:
        #         kl += prob1[i] * np.log(prob1[i] / prob2[i])
        # return kl

    def euclidean_distance(self, p1, p2):
        return np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)

    ########################
    # diameter_error_experiment
    ########################

    def diameter_experiment(self, trajectory_set1: TrajectorySet, real_tra_list: list, bucket_num=50):
        origin_diameter = self.get_real_diameter(trajectory_set1)#去重
        synthetic_diameter = self.get_synthetic_diameter(real_tra_list)

        bucket_size = (max(origin_diameter) - min(origin_diameter)) / bucket_num

        orig_count = np.zeros(bucket_num)
        syn_count = np.zeros(bucket_num)
        for i in range(bucket_num):
            start = i * bucket_size
            end = start + bucket_size

            for d in origin_diameter:
                if start <= d < end:
                    orig_count[i] += 1
            for d in synthetic_diameter:
                if start <= d < end:
                    syn_count[i] += 1

        orig_count /= np.sum(orig_count)
        syn_count /= np.sum(syn_count)
        error = self.jensen_shannon_distance(orig_count, syn_count)
        print("diameter_error:", error)

    def get_real_diameter(self, trajectory_set1: TrajectorySet) -> list:
        diameter_list = []
        trajectory_number = trajectory_set1.get_trajectory_number()
        for i in range(trajectory_number):
            trajectory1 = trajectory_set1.give_trajectory_by_index(i)
            index_array = trajectory1.experiment_array  # real_point_trajectory#experiment_array
            max_d = 0
            n = len(index_array)
            for j in range(n):
                for k in range(j + 1, n):
                    max_d = max(max_d, self.euclidean_distance(index_array[j], index_array[k]))
            diameter_list.append(max_d)
        return diameter_list

    def get_real_diameter_ex(self, T: Trajectory):
        t = T.real_point_trajectory
        max_d = 0
        for i in range(len(t)):
            for j in range(i + 1, len(t)):
                max_d = max(max_d, self.euclidean_distance(t[i], t[j]))
        return max_d

    def get_synthetic_diameter(self, real_tra_list: list) -> list:
        diameter_list = []
        for i in range(len(real_tra_list)):
            index_array = real_tra_list[i]
            max_d = 0
            n = len(index_array)
            for j in range(n):
                for k in range(j + 1, n):
                    max_d = max(max_d, self.euclidean_distance(index_array[j], index_array[k]))
            diameter_list.append(max_d)
        return diameter_list

    def get_synthetic_diameter_ex(self, t: list):
        max_d = 0
        for i in range(len(t)):
            for j in range(i + 1, len(t)):
                max_d = max(max_d, self.euclidean_distance(t[i], t[j]))
        return max_d

    def transition_pattern_experiment(self, trajectory_set1: TrajectorySet, synthetic_trajectory: list, k=200):
        origin = self.calculate_origin_transition_pattern(trajectory_set1)
        synthetic = self.calculate_synthetic_transition_pattern(synthetic_trajectory)

        sorted_origin = [(key, value) for key, value in origin.items()]
        top_n_origin = heapq.nlargest(k, sorted_origin, key=lambda item: item[1])
        origin = {key: value for key, value in top_n_origin}

        if(len(synthetic) < k):
            print("生成的迁移规模小于200，无法计算差异")
        print("原始的迁移模式数量:", len(origin))
        error = 0
        count = {2:0,3:0,4:0,5:0}
        for key in origin.keys():
            orign_value = origin[key]
            try:
                syn_value = synthetic[key]
            except KeyError:
                syn_value = 0
                count[len(key)] += 1
            error += np.abs(orign_value - syn_value) / orign_value
        error = error / k
        print("缺失的迁移模式数量:", count)
        print("transition_pattern_error:", error)

    def calculate_origin_transition_pattern(self, trajectory_set1: TrajectorySet, min_size=2, max_size=5) -> dict:
        pattern_dict = {}
        for curr_size in range(min_size, max_size + 1):
            trajectory_number = trajectory_set1.get_trajectory_number()
            for j in range(trajectory_number):
                trajectory1 = trajectory_set1.give_trajectory_by_index(j)
                index_array = trajectory1.level2_unique_index_sequence#level2_unique_index_sequence#trajectory_level2_index_sequence
                for i in range(0, len(index_array) - curr_size + 1):
                    p = tuple(index_array[i:i + curr_size])
                    pattern_dict[p] = pattern_dict.get(p, 0) + 1
        return pattern_dict

    def calculate_synthetic_transition_pattern(self, synthetic_trajectory: list, min_size=2, max_size=5) -> dict:
        pattern_dict = {}
        for curr_size in range(min_size, max_size + 1):
            for j in range(len(synthetic_trajectory)):
                index_array = synthetic_trajectory[j]
                for i in range(0, len(index_array) - curr_size + 1):
                    p = tuple(index_array[i:i + curr_size])
                    pattern_dict[p] = pattern_dict.get(p, 0) + 1
        return pattern_dict
